package seismosurfer.database;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

import seismosurfer.util.Registry;
import seismosurfer.util.SeismoException;

import com.bbn.openmap.util.Debug;

/**
 * Helper class that handles database connection management. Helps in
 * opening and closing db connections, starting and commiting transactions
 * and cleaning up db resources.
 *
 */
public class DB {
    private static final int NO_LEVEL = -1;
    
    public static final int NO_LIMIT = 0;

    /**
     * If the value of this constant is set in the 
     * properties file, debugging messages are printed.
     * 
     */
    public static final String DEBUG = "DB";

    private Map connections = Collections.synchronizedMap(new HashMap());    

    private DataSource dataSource = null;

    private static DB db = new DB();

    /**
     * Protected default constructor to disable
     * uncontrolled instantiation of this class.
     *
     */
    protected DB() {
    }

    /**
     * Initializes the database connection pool.
     *
     */
    public static void init() {
        db.initDataSource();
    }

    
    protected static String getDataSourceName() {
        return Registry.getDBDataSourceName();
    }    

    /**
     * Initializes the datasource object.
     *
     */
    protected void initDataSource() {

        // Load the Oracle JDBC driver
        try {
            Context initCtx = new InitialContext();
            Context envCtx = (Context) initCtx.lookup("java:comp/env");
            db.dataSource = (DataSource) envCtx.lookup(getDataSourceName());
        } catch (NamingException e) {
            throw new SeismoException(e);
        }

        Debug.output("Initialized DataSource:" + db.dataSource.toString());

    }

    /**
     * Gets a connection from the connection pool, which can be used
     * by the current request (thread).
     *
     */
    public static void openConnection() {
        Connection con = null;
        try {
            con = db.dataSource.getConnection();
            db.connections.put(Thread.currentThread(), con);

            if (Debug.debugging(DEBUG)) {
                Debug.output("Got connection for thread:"
                        + Thread.currentThread());
            }
        } catch (SQLException ex) {
            throw new SeismoException(ex);
        }

    }

    /**
     * Closes a connection, making it available for other
     * requests (threads).
     *
     */
    public static void closeConnection() {
        Connection con = null;
        try {
            con = getCurrentConnection();
            con.close();
            db.connections.remove(Thread.currentThread());

            if (Debug.debugging(DEBUG)) {
                Debug.output("Removed connection for thread:"
                        + Thread.currentThread());
            }
        } catch (SQLException ex) {
            throw new SeismoException(ex);
        }
    }

    /**
     * Given a sql statement it returns a PreparedStatement object.
     * 
     * @param sql the text that contains the sql statement
     * @return a corresponding PreparedStatement object
     * @throws SQLException
     */
    public static PreparedStatement prepare(String sql) throws SQLException {
        Connection con = null;
        con = getCurrentConnection();
        return con.prepareStatement(sql);
    }

    /**
     * Begins a transaction and sets its isolation level.
     * 
     * @param level the transaction isolation level
     * @throws SQLException
     */
    public static void begin(int level) throws SQLException {
        Connection con = getCurrentConnection();
        con.setAutoCommit(false);

        if (level != NO_LEVEL) {
            con.setTransactionIsolation(level);
        }

        if (Debug.debugging(DEBUG)) {
            Debug.output("Beginning transaction...");
        }
    }

    /**
     * Begins a transaction. 
     *  
     * @throws SQLException
     */
    public static void begin() throws SQLException {
        begin(NO_LEVEL);
    }

    /**
     * Commits the transaction.
     * 
     * @throws SQLException
     */
    public static void commit() throws SQLException {
        Connection con = getCurrentConnection();
        con.commit();

        if (Debug.debugging(DEBUG)) {
            Debug.output("Transaction commited...");
        }
    }

    /**
     * Rolls back the transaction.
     * 
     * @throws SQLException
     */
    public static void rollback() throws SQLException {
        Connection con = getCurrentConnection();
        con.rollback();

        if (Debug.debugging(DEBUG)) {
            Debug.output("Transaction rolled back...");
        }
    }

    /**
     * Handles a transaction error and translates 
     * system exceptions to application exceptions.
     * 
     * @param e the SQLException that occurred
     * @throws SeismoException
     */
    public static void handleTransactionError(SQLException e)
            throws SeismoException {
        try {
            DB.rollback();
            throw new SeismoException(e);
        } catch (SQLException ex) {
            throw new SeismoException(ex);
        }
    }

    /**
     * Gets a reference to the current request`s connection.
     * 
     * @return a reference to the current request`s connection
     * @throws SQLException
     */
    public static Connection getCurrentConnection() throws SQLException {
        Connection con = (Connection) db.connections
                .get(Thread.currentThread());

        if (closedOrNull(con)) {
            throw new SeismoException(
                    "Connection was closed or is null. Please see the logs.");
        }
        return con;
    }

    /**
     * Determines if a connection is closed or null.
     * 
     * @param con the Connection to be examined
     * @return true if it is closed or null, false otherwise
     * @throws SQLException
     */
    protected static boolean closedOrNull(Connection con) throws SQLException {
        return ((con == null) || (con.isClosed()));
    }

    /**
     * Closes the given PreparedStatement and ResultSet object.
     * 
     * @param stm the PreparedStatement to be closed
     * @param rs the ResultSet to be closed
     */
    public static void cleanUp(PreparedStatement stm, ResultSet rs) {

        try {
            if (rs != null) {
                rs.close();
            }

            if (stm != null) {
                stm.close();
            }

        } catch (SQLException ex) {
            throw new SeismoException(ex);
        }
    }

    /**
     * Closes the given PreparedStatement object.
     * 
     * @param stm the PreparedStatement to be closed 
     */
    public static void cleanUp(PreparedStatement stm) {
        cleanUp(stm, null);
    }

}